Micron Document
🎖️GitЯра🎖️

Commit 63ff12dac345135d952c92d93d9063737300f9ce


Parents : d264b40
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-01T05:31:58-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-01T12:31:58Z

fix(node): restore view-tree owners on map dispose so node-list popups aren't invisible (#5699)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Changes
Diff

diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
index 3efaf68ef2..a7ae3b8e20 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
@@ -24,8 +24,10 @@ import androidx.compose.ui.platform.LocalView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.currentStateAsState
+import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.view.DefaultClusterRenderer
@@ -58,16 +60,31 @@ fun NodeClusterMarkers(
// so the internal snapshot view can find them when walking up the tree.
// DisposableEffect runs at composition time (not post-composition like SideEffect),
// ensuring owners are set before the Clustering composable triggers marker rendering.
+ // Capture and restore the previous owners on dispose. The owners here are NavEntry-scoped
+ // (transient) lifecycles; leaving one attached to the activity root view after this screen is
+ // destroyed makes subsequently opened Popups/DropdownMenus inherit a DESTROYED lifecycle and
+ // render at 0x0 (invisible). See the node-list popup regression and InlineMap.
DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
+ val prevRootLifecycleOwner = root.findViewTreeLifecycleOwner()
+ val prevRootSavedStateRegistryOwner = root.findViewTreeSavedStateRegistryOwner()
root.setViewTreeLifecycleOwner(lifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
// Also set on the view itself in case the internal renderer walks from a child
+ val prevViewLifecycleOwner = view.findViewTreeLifecycleOwner()
+ val prevViewSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
if (view !== root) {
view.setViewTreeLifecycleOwner(lifecycleOwner)
view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
}
- onDispose {}
+ onDispose {
+ root.setViewTreeLifecycleOwner(prevRootLifecycleOwner)
+ root.setViewTreeSavedStateRegistryOwner(prevRootSavedStateRegistryOwner)
+ if (view !== root) {
+ view.setViewTreeLifecycleOwner(prevViewLifecycleOwner)
+ view.setViewTreeSavedStateRegistryOwner(prevViewSavedStateRegistryOwner)
+ }
+ }
}
// Guard against the cluster renderer's async Handler trying to render markers

diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
index c65c0bec1d..3ac4f605f1 100644
--- a/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
+++ b/androidApp/src/google/kotlin/org/meshtastic/app/node/component/InlineMap.kt
@@ -26,8 +26,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LocalLifecycleOwner
+import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.compose.LocalSavedStateRegistryOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
@@ -55,20 +57,28 @@ fun InlineMap(node: Node, modifier: Modifier = Modifier) {
else -> ComposeMapColorScheme.LIGHT
}
- // Workaround for maps-compose issue where MarkerComposable's internal ComposeView
- // cannot find ViewTreeLifecycleOwner, causing crash on bitmap rendering.
+ // Workaround for a maps-compose issue where MarkerComposable's internal ComposeView cannot find a
+ // ViewTreeLifecycleOwner, causing a crash on bitmap rendering. We attach the current owners to the
+ // window root view for the lifetime of the map.
+ //
+ // IMPORTANT: capture and restore the previous owners on dispose. This InlineMap is hosted inside the
+ // node-detail NavEntry, whose LocalLifecycleOwner is a transient, entry-scoped lifecycle. Leaving it
+ // attached to the activity root view after the entry is destroyed (e.g. navigating back to the node
+ // list) would make every subsequently opened Popup/DropdownMenu inherit a DESTROYED lifecycle and
+ // render at 0x0 (invisible). See the node-list popup regression.
val view = LocalView.current
val lifecycleOwner = LocalLifecycleOwner.current
val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current
DisposableEffect(lifecycleOwner, savedStateRegistryOwner) {
val root = view.rootView
+ val prevRootLifecycleOwner = root.findViewTreeLifecycleOwner()
+ val prevRootSavedStateRegistryOwner = root.findViewTreeSavedStateRegistryOwner()
root.setViewTreeLifecycleOwner(lifecycleOwner)
root.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
- if (view !== root) {
- view.setViewTreeLifecycleOwner(lifecycleOwner)
- view.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner)
+ onDispose {
+ root.setViewTreeLifecycleOwner(prevRootLifecycleOwner)
+ root.setViewTreeSavedStateRegistryOwner(prevRootSavedStateRegistryOwner)
}
- onDispose {}
}
key(node.num) {

Served by rngit 1.5.0 - Generated in 0.05s